﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using HtmlAgilityPack;
using NLog;
using SellAvi.Model;
using SellAvi.Properties;
using SellAvi.Schemas;

namespace SellAvi.Classes
{
    class AvitoParserProcessor
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
        private string _currentProcessingPage;
        /// <summary>
        /// Текущая обрабатываемая страница в методе async Task ...
        /// </summary>
        public string CurrentProcessingPage
        {
            get { return _currentProcessingPage; }
            private set { _currentProcessingPage = value; }
        }

        /// <summary>
        /// Схема парсинга сайта с настройками
        /// </summary>
        private readonly ParserSchema _sps;

        /// <summary>
        /// Вебклиент для загрузки веб-страниц
        /// </summary>
        private readonly WebClientEx webClient;


        public AvitoParserProcessor(WebClientEx wc, ParserSchema ps)
        {
            webClient = wc;
            _sps = ps;
            //Создание директории для спарсеных изображений
            Directory.CreateDirectory("images");
            Directory.CreateDirectory("phones");
        }

        /// <summary>
        /// Получение (парсинг) списка ссылок на продукты из содержимого индексной страницы
        /// </summary>
        /// <param name="htmlString">содержимое веб-страницы</param>
        /// <returns></returns>
        private List<AvitoSearchItem> ParseIndex(string htmlString)
        {
            if (htmlString == null)
                return null;
            var htmlPage = new HtmlDocument();
            var parsedItems = new List<AvitoSearchItem>();

            htmlPage.LoadHtml(htmlString);
            HtmlNodeCollection siteRowNodes = htmlPage.DocumentNode.SelectNodes(_sps.XPathPublishUrl);
            if (siteRowNodes != null)
            {
                foreach (HtmlNode node in siteRowNodes)
                {
                    var item = new AvitoSearchItem
                    {
                        AdUrl = new Uri(new Uri(_sps.SiteUrl), node.Attributes["href"].Value),
                        AdTitle = HttpUtility.HtmlDecode(node.InnerText.Trim(new char[] { '\n', ' ' }))

                    };
                    parsedItems.Add(item);
                }
            }
            else
            {
                Logger.Warn("Настройка парсинга объявлений ({0}) не смогла распознать содержимое страницы", _sps.XPathPublishUrl);
            }
            return parsedItems;
        }


        /// <summary>
        /// Асинхроная загрузка списка ссылок со страницы с индексом по предоставленому url 
        /// </summary>
        /// <returns></returns>
        public async Task<List<AvitoSearchItem>> DownloadIndexUrlsAsync(Uri url) //? почему не используется SPS.DefaultParseUrl ?
        {
            await Task.Delay(TimeSpan.FromMilliseconds(new Random().Next(1000, 5000)), new CancellationToken()).ConfigureAwait(false);
            return ParseIndex(await webClient.DownloadStringTaskAsync(url));
        }

        /// <summary>
        /// Загрузка и парсинг страницы с товаром (Асинхронный вызов)
        /// </summary>
        /// <param name="url">Страница на товар</param>
        /// <returns>AvitoProduct</returns>
        //public async Task<AvitoProduct> GetAvitoProductByLinkAsync(Uri url)
        //{
        //    _currentProcessingPage = url.Segments.LastOrDefault();
        //    while (webClient.IsBusy)
        //    {
        //        Logger.Info("WebClient занят асинхронной загрузкой");
        //        await Task.Delay(TimeSpan.FromMilliseconds(sps.AntiBanTime/10), new CancellationToken()).ConfigureAwait(false);

        //    }

        //    var htmlInner = await webClient.DownloadStringTaskAsync(url);
        //    AvitoProduct ap = CreateProductContent(htmlInner);
        //    ap.ParsedFromUrl = url.ToString();
        //    return ap;


        //}
        public AvitoProduct GetAvitoProductByLink(Uri url)
        {
            _currentProcessingPage = url.Segments.LastOrDefault();
            var htmlInner = webClient.DownloadString(url);
            AvitoProduct ap = CreateProductContent(htmlInner);
            ap.ParsedFromUrl = url.ToString();
            return ap;
        }



        /// <summary>
        /// Создание продукта из содержимого HTML страницы
        /// </summary>
        /// <param name="strReturn">Сожержимое веб-страницы</param>
        /// <returns></returns>
        private AvitoProduct CreateProductContent(string strReturn)
        {

            try
            {
                if (strReturn == null)
                    return null;
                var htmlPage = new HtmlDocument();
                var newProduct = new AvitoProduct();


                htmlPage.LoadHtml(strReturn);
                var propertyNodes = htmlPage.DocumentNode.SelectNodes("//@itemprop");
                var desc = propertyNodes.LastOrDefault(s => s.GetAttributeValue("itemprop", "") == "description").InnerHtml.Trim();
                newProduct.BreadCrumbsCategory = htmlPage.DocumentNode.SelectSingleNode("//div[@class='b-catalog-breadcrumbs']/a[last()]").Attributes["href"].Value;
                newProduct.Title = HttpUtility.HtmlDecode(htmlPage.DocumentNode.SelectNodes("//h1[@class='title-info-title']")[0].InnerText.Trim());
                newProduct.Description = Settings.Default.ParserTrimHtml == true
                    ? Regex.Replace(HttpUtility.HtmlDecode(desc).Replace("<br>", "\r\n"), @"<[^>]*>", String.Empty)
                    : HttpUtility.HtmlDecode(desc);



                // Цена
                var hasPrice = htmlPage.DocumentNode.SelectNodes("//span[contains(@class, 'price-value-string')]");

                if (hasPrice != null)
                {
                    string priceText;
                    if (Settings.Default.ParserTrimPrice)
                    {
                        priceText = hasPrice[0].InnerText
                        .Replace("&nbsp;", "")
                        .Replace(" руб.", "")
                        .Replace(" ", "")
                        .Replace("\n", "")
                        .Replace("&thinsp;", "")
                        .Replace("₽", "");
                        priceText = Regex.Match(priceText, @"\d+").Value;

                    }
                    else
                    {
                        priceText = hasPrice[0].InnerText.Trim(' ', '\r', '\n');
                    }
                    newProduct.Price = priceText;
                }




                // Изображение
                var hasImage = htmlPage.DocumentNode.SelectNodes("//div[contains(@class,'gallery-img-frame')]");
                if (hasImage != null)
                {
                    var imgUrl = new Uri("http:" + hasImage[0].GetAttributeValue("data-url", ""));
                    newProduct.AvitoImages = new List<AvitoImage>(1) { new AvitoImage() { RemoteFileUrl = imgUrl.ToString() } };
                }

                // Если происходит парсинг ЛК
                if (_sps.DefaultParseUrl.ToString().Contains("profile"))
                {
                    newProduct.IsProfileProduct = true;
                }


                return newProduct;
            }
            catch (NullReferenceException ex)
            {
                throw new FormatChangedException(ex.Message);
            }
        }

        /// <summary>
        /// Загрузчик изображений к товарам
        /// </summary>
        /// <param name="ap">AvitoProduct</param>
        public void DownloadProductImage(AvitoProduct ap)
        {
            var firstImage = ap.AvitoImages.FirstOrDefault();
            if (firstImage != null)
            {
                var imgUrl = new Uri(firstImage.RemoteFileUrl);
                var localFileName = Guid.NewGuid() + new FileInfo(imgUrl.AbsolutePath).Extension;
                webClient.DownloadFile(imgUrl, "images/" + localFileName);
                firstImage.LocalFileName = localFileName;
                Logger.Info("Загружено изображение: {0}", localFileName);
            }
        }


        //public async Task<string> DownloadUserPhoneAsync(AvitoProduct ap)
        public string DownloadUserPhone(AvitoProduct ap)
        {
            var query = new Uri(ap.ParsedFromUrl).AbsolutePath;
            var mobileUrl = new Uri(new Uri("https://m.avito.ru"), query);

            var htmlInner = webClient.DownloadString(mobileUrl);
            var phoneLinkRegex = new Regex(query + @"/phone/[a-f0-9]+");

            webClient.DefaultHeaders[HttpRequestHeader.Referer] = mobileUrl.ToString();

            var phoneQuery = phoneLinkRegex.Match(htmlInner).Value;
            var phoneUrl = new Uri(new Uri("https://m.avito.ru"), phoneQuery + "?async");
            var phoneInner = webClient.DownloadString(phoneUrl);

            var phoneRegex = new Regex("[-0-9 ]{15}");
            var phoneNumber = phoneRegex.Match(phoneInner).Value;


            if (phoneInner.Contains("обновите") || String.IsNullOrEmpty(phoneNumber.Trim()))
            {
                //TODO: обработать ситуацию: "Доступ с Вашего IP временно ограничен"
                Logger.Warn("Получен неверный ответ: {0}", phoneInner);
                return null;
            }

            Logger.Info("Получен номер телефона: {0}", phoneNumber);
            return phoneNumber;

        }

        public string DownloadUserPhone2(AvitoProduct ap)
        {
            var query = new Uri(ap.ParsedFromUrl).AbsolutePath;
            var mobileUrl = new Uri(new Uri("https://m.avito.ru"), query);
            var htmlInner = webClient.DownloadString(mobileUrl);
            var phoneKeyRegex = new Regex(query + @"/phone/([a-f0-9]+)");

            var productIdRegex = new Regex(@"\d+$");
            var productId = productIdRegex.Match(ap.ParsedFromUrl).Value;

            webClient.DefaultHeaders[HttpRequestHeader.Referer] = ap.ParsedFromUrl;
            var phoneKey = phoneKeyRegex.Match(htmlInner).Groups[1].Value;
            var phoneUrl = new Uri(String.Format("https://www.avito.ru/items/phone/{0}?pkey={1}", productId, phoneKey));
            var phoneInner = webClient.DownloadString(phoneUrl);

            //var base64Regex = new Regex(@"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$");
            var base64Regex = new Regex(@"([A-Za-z0-9+/=]+)\""\}");
            var phoneFileName = String.Format("phones/{0}.png", productId);
            var base64String = base64Regex.Match(phoneInner).Groups[1].Value;
            File.WriteAllBytes(phoneFileName, Convert.FromBase64String(base64String));

            return PhoneRecognition(phoneFileName);
        }

        public string DownloadUserPhone3(AvitoProduct ap)
        {
            var productIdRegex = new Regex(@"\d+$");
            var productId = productIdRegex.Match(ap.ParsedFromUrl).Value;

            var key = "XcyLEIoAuUgGSJUDFAs4xWTnV2TKEmvZTZuTtPFr";
            var secret = "LLI1/E9&zfG-wwKTRd[XGFOAH~u.pnEJ{q(=oyAG";
            var deviceId = "861087f9e82327cc";
            var path = String.Format("/items/{0}", productId);

            var payload = String.Format("deviceId={0}&includeRefs=1", deviceId);

            var sum = CreateMD5(CreateMD5(secret) + CreateMD5(path) + CreateMD5(payload));
            var phoneUrl = new Uri(String.Format("https://www.avito.ru/api/2{0}?checksum={1}&{2}&key={3}", path, sum, payload, key));
            var phoneInner = webClient.DownloadString(phoneUrl);

            var phoneRegex = new Regex("[-0-9 ]{15}");
            var phoneNumber = phoneRegex.Match(phoneInner).Value;

            return phoneNumber;
        }
        public static string CreateMD5(string input)
        {
            // Use input string to calculate MD5 hash
            using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
            {
                byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
                byte[] hashBytes = md5.ComputeHash(inputBytes);

                // Convert the byte array to hexadecimal string
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < hashBytes.Length; i++)
                {
                    sb.Append(hashBytes[i].ToString("X2"));
                }
                return sb.ToString().ToLower();
            }
        }

        /// <summary>
        /// <see cref="https://github.com/tesseract-ocr/tesseract/wiki/AddOns#tesseract-wrappers"/>
        /// <see cref="https://github.com/charlesw/tesseract"/>
        /// <see cref="http://vietocr.sourceforge.net/training.html"/>
        /// </summary>
        /// <param name="filename"></param>
        /// <returns></returns>
        public string PhoneRecognition(string filename)
        {
            //var correntPhoneRegex = new Regex(@"^8 \d{3} \d{3}-\d{2}-\d{2}");
            var digitRegex = new Regex(@"\d");
            var parsedText = TesseractOcr.TesseractOcr.StartOcr(filename).TrimEnd('\r', '\n');
            Logger.Info(String.Format("Вероятность распознавания {0}, телефонный номер: {1}",
                TesseractOcr.TesseractOcr.MeanConfidence, parsedText));
            var phoneDigitString = digitRegex.Matches(parsedText)
                .Cast<Match>()
                .Select(match => match.Value)
                .Aggregate((total, part) => total + part);
            //введено из-за некорректно распознавания символа "-" при обработке OCR
            if (phoneDigitString.Length == 11 && phoneDigitString[0] == '8')
            {
                phoneDigitString = phoneDigitString.Insert(1, " ");
                phoneDigitString = phoneDigitString.Insert(5, " ");
                phoneDigitString = phoneDigitString.Insert(9, "-");
                phoneDigitString = phoneDigitString.Insert(12, "-");
                return phoneDigitString;
            }
            return null;

        }


        [Serializable]
        public class FormatChangedException : Exception
        {
            public FormatChangedException()
            { }

            public FormatChangedException(string message)
                : base(message)
            { }

            public FormatChangedException(string message, Exception innerException)
                : base(message, innerException)
            { }
        }


    }
}
